TUI File Explorer

use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
    Frame, Terminal,
    backend::{Backend, CrosstermBackend},
    layout::{Constraint, Direction, Layout},
    style::{Color, Modifier, Style},
    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
};
use std::{
    env, fs, io,
    path::{Path, PathBuf},
    time::Duration,
};

struct TreeEntry {
    entry: fs::DirEntry,
    depth: usize,
}

struct App {
    should_quit: bool,
    recursive_view: bool,
    current_path: PathBuf,
    current_entries: Vec<TreeEntry>,
    current_selected: ListState,
    parent_entries: Vec<fs::DirEntry>,
    parent_selected: ListState,
}

impl App {
    fn new() -> Self {
        let mut app = Self {
            should_quit: false,
            recursive_view: false,
            current_path: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
            current_entries: Vec::new(),
            current_selected: ListState::default(),
            parent_entries: Vec::new(),
            parent_selected: ListState::default(),
        };
        app.current_selected.select(Some(0));
        app.update_panels();
        app
    }

    fn read_dir_entries(path: &Path) -> io::Result<Vec<fs::DirEntry>> {
        let mut entries = fs::read_dir(path)?
            .filter_map(|res| res.ok())
            .collect::<Vec<_>>();

        entries.sort_by_key(|entry| {
            let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
            (!is_dir, entry.file_name())
        });
        Ok(entries)
    }

    fn build_recursive_tree(
        path: &Path,
        current_depth: usize,
        entries_list: &mut Vec<TreeEntry>,
    ) -> io::Result<()> {
        let entries = Self::read_dir_entries(path)?;

        for entry in entries {
            let path = entry.path();
            let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);

            entries_list.push(TreeEntry {
                entry,
                depth: current_depth,
            });

            if is_dir {
                let _ = Self::build_recursive_tree(&path, current_depth + 1, entries_list);
            }
        }
        Ok(())
    }

    fn update_panels(&mut self) {
        self.current_entries.clear();

        if self.recursive_view {
            let _ = Self::build_recursive_tree(&self.current_path, 0, &mut self.current_entries);
        } else {
            if let Ok(entries) = Self::read_dir_entries(&self.current_path) {
                for entry in entries {
                    self.current_entries.push(TreeEntry { entry, depth: 0 });
                }
            }
        }

        if self.current_entries.is_empty() {
            self.current_selected.select(None);
        } else {
            // Select 0, or clamp to new max
            let new_max = self.current_entries.len() - 1;
            if let Some(selected) = self.current_selected.selected() {
                if selected > new_max {
                    self.current_selected.select(Some(new_max));
                }
            } else {
                self.current_selected.select(Some(0));
            }
        }

        if let Some(parent_path) = self.current_path.parent() {
            self.parent_entries = Self::read_dir_entries(parent_path).unwrap_or_default();
        } else {
            self.parent_entries.clear();
        }
        self.parent_selected.select(Some(0));
    }

    fn toggle_recursive_view(&mut self) {
        self.recursive_view = !self.recursive_view;
        self.update_panels();
    }

    fn toggle_fold(&mut self) {
        let selected_idx = match self.current_selected.selected() {
            Some(i) => i,
            None => return,
        };

        let (current_path, current_depth) = {
            let selected_item = &self.current_entries[selected_idx];

            if !selected_item
                .entry
                .file_type()
                .map(|ft| ft.is_dir())
                .unwrap_or(false)
            {
                return;
            }
            (selected_item.entry.path(), selected_item.depth)
        };

        let is_unfolded = self
            .current_entries
            .get(selected_idx + 1)
            .map_or(false, |next_item| next_item.depth > current_depth);

        if is_unfolded {
            let end_range = self
                .current_entries
                .iter()
                .skip(selected_idx + 1)
                .position(|item| item.depth <= current_depth)
                .map_or(self.current_entries.len(), |i| i + selected_idx + 1);

            if end_range > selected_idx + 1 {
                self.current_entries.drain(selected_idx + 1..end_range);
            }
        } else {
            let mut new_entries = Vec::new();
            let _ = Self::build_recursive_tree(&current_path, current_depth + 1, &mut new_entries);

            if !new_entries.is_empty() {
                self.current_entries
                    .splice(selected_idx + 1..selected_idx + 1, new_entries);
            }
        }
    }
    fn enter_directory(&mut self) {
        if let Some(selected_idx) = self.current_selected.selected() {
            if let Some(tree_entry) = self.current_entries.get(selected_idx) {
                if tree_entry
                    .entry
                    .file_type()
                    .map(|ft| ft.is_dir())
                    .unwrap_or(false)
                {
                    self.current_path = tree_entry.entry.path();
                    self.recursive_view = false;
                    self.update_panels();
                }
            }
        }
    }

    fn leave_directory(&mut self) {
        if self.current_path.pop() {
            self.recursive_view = false;
            self.update_panels();
        }
    }

    fn select_next(&mut self) {
        let i = match self.current_selected.selected() {
            Some(i) => {
                if i >= self.current_entries.len() - 1 {
                    0
                } else {
                    i + 1
                }
            }
            None => 0,
        };
        self.current_selected.select(Some(i));
    }

    fn select_previous(&mut self) {
        let i = match self.current_selected.selected() {
            Some(i) => {
                if i == 0 {
                    if self.current_entries.is_empty() {
                        0
                    } else {
                        self.current_entries.len() - 1
                    }
                } else {
                    i - 1
                }
            }
            None => 0,
        };
        self.current_selected.select(Some(i));
    }

    fn get_selected_entry(&self) -> Option<&fs::DirEntry> {
        self.current_selected
            .selected()
            .and_then(|i| self.current_entries.get(i))
            .map(|tree_entry| &tree_entry.entry) // Get the inner entry
    }
}

fn main() -> Result<(), io::Error> {
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    let mut app = App::new();
    let res = run_app(&mut terminal, &mut app);

    disable_raw_mode()?;
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )?;
    terminal.show_cursor()?;

    if let Err(err) = res {
        println!("{err:?}");
    }

    Ok(())
}

fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
    loop {
        terminal.draw(|f| ui::<B>(f, app))?;

        if event::poll(Duration::from_millis(250))? {
            if let Event::Key(key) = event::read()? {
                match key.code {
                    KeyCode::Char('q') => app.should_quit = true,
                    KeyCode::Char('e') => app.toggle_recursive_view(),
                    KeyCode::Char('t') => app.toggle_fold(),
                    KeyCode::Char('j') | KeyCode::Down => app.select_next(),
                    KeyCode::Char('k') | KeyCode::Up => app.select_previous(),
                    KeyCode::Char('h') | KeyCode::Backspace | KeyCode::Left => {
                        app.leave_directory()
                    }
                    KeyCode::Char('l') | KeyCode::Enter | KeyCode::Right => app.enter_directory(),
                    _ => {}
                }
            }
        }

        if app.should_quit {
            return Ok(());
        }
    }
}

fn ui<B: Backend>(f: &mut Frame, app: &mut App) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(1), Constraint::Min(0)])
        .split(f.size());

    let header_chunk = chunks[0];
    let content_chunk = chunks[1];

    let path_str = app.current_path.to_string_lossy();
    let header =
        Paragraph::new(path_str.as_ref()).style(Style::default().bg(Color::Blue).fg(Color::White));
    f.render_widget(header, header_chunk);

    let content_chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage(25),
            Constraint::Percentage(40),
            Constraint::Percentage(35),
        ])
        .split(content_chunk);

    let parent_items: Vec<ListItem> = app
        .parent_entries
        .iter()
        .map(|entry| format_entry_flat(entry))
        .collect();
    let parent_list = List::new(parent_items)
        .block(Block::default().borders(Borders::ALL).title("Parent"))
        .style(Style::default().fg(Color::DarkGray));
    f.render_stateful_widget(parent_list, content_chunks[0], &mut app.parent_selected);

    let title = if app.recursive_view {
        "Current (Recursive 'e')"
    } else {
        "Current (Flat 'e')"
    };
    let current_items: Vec<ListItem> = app
        .current_entries
        .iter()
        .map(|tree_entry| format_entry_tree(tree_entry))
        .collect();
    let current_list = List::new(current_items)
        .block(Block::default().borders(Borders::ALL).title(title))
        .highlight_style(
            Style::default()
                .bg(Color::LightBlue)
                .fg(Color::Black)
                .add_modifier(Modifier::BOLD),
        );
    f.render_stateful_widget(current_list, content_chunks[1], &mut app.current_selected);

    let preview_text = match app.get_selected_entry() {
        Some(entry) => get_entry_info(entry),
        None => "No item selected".to_string(),
    };
    let preview = Paragraph::new(preview_text)
        .block(Block::default().borders(Borders::ALL).title("Preview"))
        .wrap(ratatui::widgets::Wrap { trim: false });
    f.render_widget(preview, content_chunks[2]);
}

fn format_entry_flat(entry: &fs::DirEntry) -> ListItem {
    let file_name = entry.file_name().to_string_lossy().to_string();
    let metadata = entry.metadata().ok();
    let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);

    let (icon, style) = if is_dir {
        ("📁 ", Style::default().fg(Color::Cyan))
    } else {
        ("📄 ", Style::default().fg(Color::White))
    };

    ListItem::new(format!("{icon}{file_name}")).style(style)
}

fn format_entry_tree(tree_entry: &TreeEntry) -> ListItem {
    let entry = &tree_entry.entry;
    let file_name = entry.file_name().to_string_lossy().to_string();
    let metadata = entry.metadata().ok();
    let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);

    let (icon, style) = if is_dir {
        ("📁 ", Style::default().fg(Color::Cyan)) // Directory
    } else {
        ("📄 ", Style::default().fg(Color::White)) // File
    };

    let indent = "  ".repeat(tree_entry.depth);

    ListItem::new(format!("{indent}{icon}{file_name}")).style(style)
}

fn get_entry_info(entry: &fs::DirEntry) -> String {
    let mut info = String::new();
    info.push_str(&format!("Name: {}\n", entry.file_name().to_string_lossy()));

    if let Ok(metadata) = entry.metadata() {
        let file_type = if metadata.is_dir() {
            "Directory"
        } else if metadata.is_file() {
            "File"
        } else if metadata.is_symlink() {
            "Symlink"
        } else {
            "Other"
        };
        info.push_str(&format!("Type: {}\n", file_type));

        if metadata.is_file() {
            info.push_str(&format!("Size: {}\n", format_size(metadata.len())));
        }

        if let Ok(modified) = metadata.modified() {
            if let Ok(duration) = modified.duration_since(std::time::SystemTime::UNIX_EPOCH) {
                info.push_str(&format!("Modified (epoch): {}\n", duration.as_secs()));
            }
        }

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = metadata.permissions();
            info.push_str(&format!("Perms: {:o}\n", perms.mode() & 0o777));
        }
    } else {
        info.push_str("Could not read metadata.\n");
    }

    info
}

fn format_size(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;

    if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}